Skip to content

Add AuthenticationComponent::replaceIdentity()#788

Open
dereuromark wants to merge 3 commits into4.xfrom
feat/replace-identity
Open

Add AuthenticationComponent::replaceIdentity()#788
dereuromark wants to merge 3 commits into4.xfrom
feat/replace-identity

Conversation

@dereuromark
Copy link
Copy Markdown
Member

@dereuromark dereuromark commented Apr 28, 2026

Summary

Adds AuthenticationComponent::replaceIdentity() for swapping the in-request identity attribute without going through clearIdentity() / persistIdentity().

Motivation

A common pattern in apps is to enrich the active user with eager-loaded associations or computed flags in AppController::beforeFilter(). Doing this with setIdentity() has a surprising side effect: setIdentity() calls clearIdentity() first, and the service's clearIdentity() actively calls stopImpersonating() on impersonation-aware authenticators (AuthenticationService.php lines 192-200). So a pattern like

if ($user && !$user->some_association) {
    $reloaded = $this->Users->get($user->id, finder: 'fullProfile');
    $this->Authentication->setIdentity($reloaded);
}

silently ends impersonation on every request where the association was missing.

replaceIdentity() solves this by writing only to the request attribute - no session writes, no clearIdentity chain, no impersonation interaction. It mirrors what withAttribute('identity', new Identity($data)) does, but routes through the service so the configured identityClass is honored.

Changes

  • AuthenticationComponent::replaceIdentity(ArrayAccess|array $identity) - new method
  • AuthenticationServiceInterface::buildIdentity() - lifted to the interface so the component can call it without a concrete-type cast (the method already existed on AuthenticationService)
  • 3 unit tests: array data, identity instance, and the impersonation-survival case

Swaps the in-request identity attribute without going through
clearIdentity()/persistIdentity(). Useful for cache-warming the
active identity (eager-loaded associations, computed flags) without
ending impersonation or rotating the session.

Adds AuthenticationServiceInterface::buildIdentity() so the
component can build an identity object using the configured
identityClass via the public service API.
@dereuromark dereuromark requested a review from LordSimal April 28, 2026 20:58
Comment thread src/AuthenticationServiceInterface.php Outdated
@ADmad
Copy link
Copy Markdown
Member

ADmad commented Apr 29, 2026

The fact that AuthenticationComponent::setIdentity() calls AuthenticationService::clearIdentity() has bitten me too.

Your proposed solution of replaceIdentity() which only updates the request attribute can also be problematic as it doesn't persist the changes. I have a use case where the identity needs to be checked and updated only on specific actions and once it's updated the updates should persist for the subsequent requests.

Maybe we need an option for AuthenticationComponent::setIdentity() or a new method which just skips the AuthenticationService::clearIdentity() call? I haven't checked but would preserve any current impersonation?


Another related issue with using AuthenticationComponent::setIdentity() is that it causes problems when you are also using the Authorization plugin. During normal flow the AuthorizationMiddleware decorates the identity object so that the identity implements the Authorization\IdentityInterface.

If you use AuthenticationComponent::setIdentity() that decoration is lost and if call any code which expects a Authorization\IdentityInterface instance it will generate an error. So we need a AuthenticationComponent::setIdentity() too (currently there's only AuthenticationComponent::buildIdentity() and then you have to update the request attribute yourself).

… option

- AuthenticationServiceInterface: revert added buildIdentity() method
  declaration and replace with a @method docblock annotation. Adding a
  method to the interface is BC-breaking for any third-party implementer
  and cannot ship in 4.x or 4.next.
- AuthenticationComponent::setIdentity() gains a $preserveImpersonation
  flag. When true, the new identity is persisted into the session as
  usual, but an active impersonation session is left intact (as is the
  successfully resolved authenticator).
- AuthenticationService::clearIdentity() gains an optional third
  $stopImpersonation parameter that backs the new behavior. The interface
  signature is unchanged, so external implementers remain compatible.
- Adds tests covering both the preserveImpersonation path and the
  default path that still ends impersonation.
@dereuromark
Copy link
Copy Markdown
Member Author

dereuromark commented Apr 29, 2026

Pushed an update that addresses both points.

On the interface BC break (per @LordSimal):
Reverted the addition of buildIdentity() on AuthenticationServiceInterface and replaced it with a @method docblock annotation on the interface, so the call site stays type-aware without breaking third-party implementers.

On your "needs to persist for subsequent requests" point:
You're right that the original replaceIdentity() only swaps the in-request attribute and is not enough for the case where the refresh has to survive into the next request - it doesn't rewrite the session, and (as I found while testing) SessionAuthenticator::persistIdentity() deliberately won't overwrite an existing Auth slot, so just skipping clearIdentity() doesn't actually persist either.

So I went with option A: a new flag on setIdentity():

$this->Authentication->setIdentity($reloaded, preserveImpersonation: true);

Implementation: AuthenticationService::clearIdentity() gets an optional third parameter bool $stopImpersonation = true. The interface signature is untouched (PHP allows adding optional params on the implementation), so external AuthenticationServiceInterface implementers remain compatible. When set to false the impersonation slot survives, but each authenticator's clearIdentity() is still called so persistIdentity() can write the new identity into the session cleanly. The _successfulAuthenticator is also preserved in this path so same-request isImpersonating() calls keep working.

The two methods now have distinct intents:

  • replaceIdentity($identity) - request-only swap, no session write (cheap, ideal for beforeFilter enrichment).
  • setIdentity($identity, preserveImpersonation: true) - persist into session and keep impersonation alive (your use case).

New tests cover both the impersonation-preserving path and a regression test confirming the default setIdentity() flow still ends impersonation.

On the Authorization\IdentityInterface decoration loss:
Agreed that's a real adjacent papercut, but I'd prefer to handle it in a separate PR/issue so this one stays focused. Happy to open one if there isn't already.

Why I deferred it

There are three possible fixes and none are 1-line:

  1. Make Authentication aware of Authorization. Have AuthenticationComponent::setIdentity() look up an Authorization\IdentityDecorator if the authorization service is on the request and re-wrap. Cleanest
    from the user's POV but creates a soft dependency on the authorization plugin — not great for a plugin that's supposed to work standalone.
  2. Event hook. Dispatch an Authentication.identityReplaced event after writing the new identity attribute. The authorization plugin (or app code) listens and re-decorates. Decoupled, but requires
    changes in both plugins, and listeners need access to the authorization service.
  3. Document the workaround. Tell users they need to manually re-decorate after setIdentity()/replaceIdentity():
$this->Authentication->setIdentity($reloaded);                                                                                                                                                            
if ($this->request->getAttribute('authorization')) {                   
    $identity = new IdentityDecorator(                                                                                                                                                                    
        $this->request->getAttribute('authorization'),                 
        $this->request->getAttribute('identity'),                                                                                                                                                         
    );                                                                 
    $this->setRequest($this->request->withAttribute('identity', $identity));                                                                                                                              
}  
  1. Cheap to ship, but every consumer has to remember it.

@dereuromark dereuromark added this to the 4.x milestone Apr 29, 2026
…tion

- Adds a 'Replacing the current identity' section to
  authentication-component.md covering setIdentity(), replaceIdentity()
  and the preserveImpersonation flag with usage examples.
- Adds a third 'Limitations' bullet to impersonation.md explaining that
  setIdentity()/clearIdentity() end impersonation by default and pointing
  at the two new APIs as the supported workarounds.
@dereuromark dereuromark marked this pull request as ready for review April 29, 2026 11:41
@dereuromark dereuromark requested a review from ADmad April 29, 2026 20:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants